/*
 * MIT License
 *
 * Copyright (c) 2024-2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#undef unix

#include <cstring>
#include <fstream>
#include <iostream>
#include <ostream>
#include <string>
#include <unordered_map>
#include <vector>

#define CXXOPTS_NO_EXCEPTIONS
#include "cxxopts.hpp"

#include "ark_native_engine.h"
#include "ecmascript/js_runtime_options.h"
#include "ecmascript/napi/include/jsnapi_expo.h"
#include "native_create_env.h"
#include "native_utils.h"
#include "utils/log.h"

#include "js_console.h"
#include "js_fetch.h"
#include "js_file.h"
#include "js_process.h"
#include "js_textdecoder.h"
#include "js_textencoder.h"
#include "js_timer.h"
#include "js_utils.h"
#include "log.h"
#include "napi_utils.h"
#include "utils.h"

using namespace panda::ecmascript;
using JSNApi = panda::JSNApi;

extern "C" void SetLogLevel(LogLevel level);

std::string GetHelper()
{
    std::string str;
    str.append(COMMON_HELP_HEAD_MSG);
    str.append(HELP_OPTION_MSG);
    return str;
}

#define EXTERNAL_TS_MODULE(XX) \
    XX(napi)                   \
    XX(base64)                 \
    XX(uni_kv_store)           \
    XX(inner_errors)           \
    XX(event_target)           \
    XX(formdata)               \
    XX(header)                 \
    XX(readable_stream)        \
    XX(text_decoder)           \
    XX(text_encoder)           \
    XX(url_search_params)

#ifndef DEBUG_JS_MODULE
#define BIND_MODULE_BINARY(name)                      \
    extern const char _binary_##name##_abc_content[]; \
    extern const int32_t _binary_##name##_abc_length;

EXTERNAL_TS_MODULE(BIND_MODULE_BINARY)

#undef BIND_MODULE_BINARY
#endif

#define CREATE_ERROR(vm, msg, type) panda::Exception::type((vm), panda::StringRef::NewFromUtf8((vm), (msg)))
#define N_RUNNER_THROW_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, Error))
#define N_RUNNER_THROW_RANGE_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, RangeError))
#define N_RUNNER_THROW_TYPE_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, TypeError))
#define N_RUNNER_THROW_REFERENCE_ERROR(vm, msg) \
    panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, ReferenceError));
#define N_RUNNER_THROW_SYNTAX_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, SyntaxError));

panda::Local<panda::JSValueRef> JSIsStdOut(JsiRuntimeCallInfo *info)
{
    panda::EscapeLocalScope scope(info->GetVM());
    return scope.Escape<panda::JSValueRef>(panda::BooleanRef::New(info->GetVM(), utils::IsStdOut()));
}

std::vector<char> LoadABCFile(std::string name)
{
#ifndef TS_MODULE_OUT_DIR
#define TS_MODULE_OUT_DIR "."
#endif

    std::ifstream file(std::string(TS_MODULE_OUT_DIR) + "/" + name, std::ios::binary);
    if (!file)
    {
        DEFAULT_LOGGER(FATAL) << "Cannot load module: " << name;
    }
    file.seekg(0, std::ios::end);
    std::streamsize size = file.tellg();
    file.seekg(0, std::ios::beg);

    std::vector<char> buffer(size);

    if (!file.read(buffer.data(), size))
    {
        DEFAULT_LOGGER(FATAL) << "Failed to load module: " << name;
    }

    file.close();
    return buffer;
}

class NapiRunner
{
public:
    explicit NapiRunner(panda::ecmascript::JSRuntimeOptions &runtimeOptions)
    {
        vm_ = JSNApi::CreateEcmaVM(runtimeOptions);
        if (vm_ == nullptr)
        {
            DEFAULT_LOGGER(ERROR) << "Cannot Create EcmaVM";
            return;
        }

        env_ = new ArkNativeEngine(vm_, nullptr, false);
        if (!env_)
        {
            DEFAULT_LOGGER(ERROR) << "Failed to init ArkNativeEngine";
            return;
        }
        env_->SetApiVersion(20);
    }

    ~NapiRunner()
    {
        if (timer_)
            timer_->Deinit();
        uv_check_stop(&checkHandle_);
        JSNApi::TriggerGC(vm_, JSNApi::TRIGGER_GC_TYPE::FULL_GC);
        JSNApi::TriggerGC(vm_, JSNApi::TRIGGER_GC_TYPE::SHARED_FULL_GC);
        delete env_;
        env_ = nullptr;
        if (timer_)
        {
            delete timer_;
            timer_ = nullptr;
        }
        for (auto it : loadedModules_)
            it.second.FreeGlobalHandleAddr();
        JSNApi::DestroyJSVM(vm_);
        vm_ = nullptr;
    }

    int InitLoop()
    {
        if (!vm_)
        {
            return -1;
        }
        if (!env_)
        {
            return -2;
        }
        uv_loop_s *loop = env_->GetUVLoop();
        if (!loop)
        {
            return -3;
        }
        uv_walk(loop, [](uv_handle_t *handle, void *) { uv_unref(handle); }, nullptr);
        uv_check_init(loop, &checkHandle_);
        uv_unref(reinterpret_cast<uv_handle_t *>(&checkHandle_));
        checkHandle_.data = this;
        uv_check_start(&checkHandle_, [](uv_check_t *handle) {
            auto that = reinterpret_cast<NapiRunner *>(handle->data);
            if (!JSNApi::HasPendingException(that->GetVM()))
            {
                JSNApi::ExecutePendingJob(that->GetVM());
            }
        });
        return 0;
    }

    static panda::Local<panda::JSValueRef> RequireNapi(panda::JsiRuntimeCallInfo *info)
    {
        EcmaVM *vm = info->GetVM();
        panda::EscapeLocalScope scope(vm);
        NapiRunner *runner = static_cast<NapiRunner *>(info->GetData());
        Local<panda::StringRef> modulePath(info->GetCallArgRef(0));

        if (!modulePath->IsString(vm))
        {
            N_RUNNER_THROW_TYPE_ERROR(vm, "path must be string.");
            return JSValueRef::Undefined(vm);
        }

        std::string path;
        path.resize(PATH_MAX);
        modulePath->WriteUtf8(vm, path.data(), PATH_MAX, true);
        std::string realPath = utils::GetRealPath(path.c_str());
        if (realPath == "")
        {
            N_RUNNER_THROW_RANGE_ERROR(vm, (std::string("Module is not found, path: ") + path).c_str());
            return JSValueRef::Undefined(vm);
        }

        std::string errInfo = "";
        NativeModule *module =
            NativeModuleManager::GetInstance()->LoadNativeModule(realPath.c_str(), nullptr, false, errInfo);

        Local<JSValueRef> exports(JSValueRef::Undefined(vm));
        {
            auto cached = runner->loadedModules_.find(module);
            if (cached != runner->loadedModules_.end())
                return scope.Escape(cached->second.ToLocal(vm));
        }

        if (!module)
        {
            if (errInfo != "")
            {
                N_RUNNER_THROW_RANGE_ERROR(vm, errInfo.c_str());
            }
            else
            {
                N_RUNNER_THROW_RANGE_ERROR(
                    vm, (std::string("Module dose not provide an entry, module path: ") + realPath).c_str());
            }

            return JSValueRef::Undefined(vm);
        }
        if (module->registerCallback)
        {
            panda::Local<panda::ObjectRef> exportObj = panda::ObjectRef::New(vm);
            napi_value result = module->registerCallback(reinterpret_cast<napi_env>(runner->GetEngine()),
                                                         JsValueFromLocalValue(exportObj));
            exports = LocalValueFromJsValue(result);
        }
        else
        {
            return JSValueRef::Undefined(vm);
        }
        runner->loadedModules_[module] = panda::Global<panda::JSValueRef>(vm, exports);

        return scope.Escape(exports);
    }

    static panda::Local<panda::JSValueRef> PreLoad(panda::JsiRuntimeCallInfo *info)
    {
        EcmaVM *vm = info->GetVM();
        panda::EscapeLocalScope scope(vm);
        NapiRunner *runner = static_cast<NapiRunner *>(info->GetData());
        ArkNativeEngine *engine = runner->GetEngine();
        Local<panda::StringRef> modulePath(info->GetCallArgRef(0));

        if (!modulePath->IsString(vm))
        {
            N_RUNNER_THROW_TYPE_ERROR(vm, "path must be string.");
            return JSValueRef::Undefined(vm);
        }

        std::string path;
        path.resize(PATH_MAX);
        modulePath->WriteUtf8(vm, path.data(), PATH_MAX, true);
        std::string realPath = utils::GetRealPath(path.c_str());
        if (realPath == "")
        {
            N_RUNNER_THROW_RANGE_ERROR(vm, (std::string("Module is not found, path: ") + path).c_str());
            return JSValueRef::Undefined(vm);
        }

        auto *work = new uv_work_t;
        work->data = new std::string(realPath);
        auto workCb = [](uv_work_t *work) {
            auto path = reinterpret_cast<std::string *>(work->data);
            std::string errInfo = "";
            if (NativeModuleManager::GetInstance()->LoadNativeModule(path->c_str(), nullptr, false, errInfo) == nullptr)
            {
                DEFAULT_LOGGER(WARNING) << "Failed to execute 'preLoad' on 'global': " << errInfo;
            };
            delete path;
        };
        if (uv_queue_work(engine->GetUVLoop(), work, workCb, [](uv_work_t *work, int status) { delete work; }) != EOK)
        {
            DEFAULT_LOGGER(WARNING) << "Failed to execute 'preLoad' on 'global':"
                                       " Failed to start async work.";
            delete reinterpret_cast<std::string *>(work->data);
            delete work;
        }
        return JSValueRef::Undefined(vm);
    }

    void InitNativeModule()
    {
        napi_env env = GetEnv();
        napi_value global = nullptr;
        napi_get_global(env, &global);
        Local<panda::ObjectRef> globalRef = panda::JSNApi::GetGlobalObject(vm_);

        {
            Local<panda::FunctionRef> requireNapi =
                panda::FunctionRef::New(vm_, RequireNapi, nullptr, static_cast<void *>(this));
            Local<panda::StringRef> requireName = panda::StringRef::NewFromUtf8(vm_, "requireNapi");
            globalRef->Set(vm_, requireName, requireNapi);
#ifdef BUILD_OUTPUT_DIR
            globalRef->Set(vm_, panda::StringRef::NewFromUtf8(vm_, "_buildDir"),
                           panda::StringRef::NewFromUtf8(vm_, BUILD_OUTPUT_DIR));
#endif
            Local<panda::FunctionRef> preLoad =
                panda::FunctionRef::New(vm_, PreLoad, nullptr, static_cast<void *>(this));
            globalRef->Set(vm_, panda::StringRef::NewFromUtf8(vm_, "preLoad"), preLoad);
        }

        Console::BindToGlobal(env, global);
        JSUtils::Init(env, global);
        JsFile::Init(env, global);
        JsTextDecoder::Init(env, global);
        JsTextEncoder::Init(env, global);

        timer_ = JsTimer::BindToGlobal(env, global);
        timer_->Init();

        JsFetch::BindToGlobal(env, global);
    }

    void LoadTSModule()
    {
#ifndef DEBUG_JS_MODULE
#define LOAD_TS_MODULE(name) env_->LoadArkModule(_binary_##name##_abc_content, _binary_##name##_abc_length, #name);
#else
#define LOAD_TS_MODULE(name)                                      \
    do                                                            \
    {                                                             \
        auto buffer = LoadABCFile(#name ".abc");                  \
        env_->LoadArkModule(buffer.data(), buffer.size(), #name); \
    } while (0);
#endif
        EXTERNAL_TS_MODULE(LOAD_TS_MODULE)
#undef LOAD_TS_MODULE
    }

    int RunScript(std::string path)
    {
        if (!JSNApi::Execute(vm_, path, "_GLOBAL::func_main_0"))
        {
            return -1;
        }
        if (JSNApi::HasPendingException(vm_))
            return -2;
        return 0;
    }

    int RunLoop(uv_run_mode mode)
    {
        return uv_run(env_->GetUVLoop(), mode);
    }

    inline ArkNativeEngine *GetEngine() const
    {
        return env_;
    }

    inline napi_env GetEnv() const
    {
        return reinterpret_cast<napi_env>(env_);
    }

    inline panda::ecmascript::EcmaVM *GetVM() const
    {
        return vm_;
    }

    inline ArkNativeEngine *operator->() const
    {
        return env_;
    }

private:
    panda::ecmascript::EcmaVM *vm_{nullptr};
    ArkNativeEngine *env_{nullptr};
    JsTimer *timer_{nullptr};
    std::map<NativeModule *, panda::Global<panda::JSValueRef>> loadedModules_{};
    uv_check_t checkHandle_;
};

class ArkRuntimeUtils
{
public:
    static void Init()
    {
        NativeCreateEnv::RegCreateNapiEnvCallback(Create);
        NativeCreateEnv::RegDestroyNapiEnvCallback(Destory);
    }

    static panda::ecmascript::JSRuntimeOptions &GetRuntimeOptions()
    {
        return g_runtimeOptions;
    }

private:
    static std::mutex g_arkRuntimeMapMutex;
    static std::unordered_map<pid_t, NapiRunner *> g_arkRuntimeMap;
    static panda::ecmascript::JSRuntimeOptions g_runtimeOptions;

    static napi_status Create(napi_env *env)
    {
        std::lock_guard<std::mutex> lock(g_arkRuntimeMapMutex);
        thread_local pid_t tid = gettid();
        if (g_arkRuntimeMap.find(tid) != g_arkRuntimeMap.end())
        {
            return napi_create_ark_runtime_only_one_env_per_thread;
        }
        NapiRunner *runner = new NapiRunner(g_runtimeOptions);
        if (runner == nullptr)
        {
            return napi_generic_failure;
        }
        ArkNativeEngine *engine = const_cast<ArkNativeEngine *>(runner->GetEngine());
        auto vm = runner->GetVM();
        engine->SetCleanEnv([vm]() { JSNApi::DestroyJSVM(vm); });
        g_arkRuntimeMap.insert({tid, runner});
        *env = reinterpret_cast<napi_env>(engine);
        return napi_ok;
    }

    static napi_status Destory(napi_env *env)
    {
        ArkNativeEngine *engine = reinterpret_cast<ArkNativeEngine *>(*env);
        std::lock_guard<std::mutex> lock(g_arkRuntimeMapMutex);
        for (auto [tid, runner] : g_arkRuntimeMap)
        {
            if (runner->GetEnv() == *env)
            {
                delete runner;
                g_arkRuntimeMap.erase(tid);
                *env = nullptr;
                return napi_ok;
            }
        }
        return napi_destroy_ark_runtime_env_not_exist;
    }
};

std::mutex ArkRuntimeUtils::g_arkRuntimeMapMutex;
std::unordered_map<pid_t, NapiRunner *> ArkRuntimeUtils::g_arkRuntimeMap;
panda::ecmascript::JSRuntimeOptions ArkRuntimeUtils::g_runtimeOptions;

const cxxopts::Options GetArgParser(const char *name)
{
    cxxopts::Options parser(name, "Run ark byte code file with Node-API module in cmdline.");
    auto optAdder = parser.add_options();
    optAdder("h,help", "Print the help message and exit.");
    optAdder("l,log-level", "Log level of napi_runner.", cxxopts::value<std::string>()->default_value("error"));
    optAdder("j,jit", "Enable jit for ArkJSRuntime.");
    optAdder("k,ark-options", "Options for ark js runtime.", cxxopts::value<std::vector<std::string>>());
    optAdder("file", "Path of abc file and args passing to abc file.", cxxopts::value<std::string>());
    parser.parse_positional({"file"});
    parser.positional_help("file [args...]");
    return parser;
}

int GetAbcArgIndex(int argc, const char *argv[])
{
    const char *suf = ".abc";
    int index = 1;
    std::string arg;
    for (; index < argc; index++)
    {
        arg = argv[index];
        if (utils::EndsWith(arg, suf))
            return index;
    }
    return 0;
}

int main(int argc, const char *argv[])
{
    cxxopts::Options parser = GetArgParser(argv[0]);
    if (argc < 2) // 2: at least have two arguments
    {
        std::cerr << parser.help() << std::endl;
        return 0;
    }

    int index = GetAbcArgIndex(argc, argv);
    std::string file = argv[index];
    if (index <= 0)
    {
        std::cerr << "path of abc file must be provided.\n" << parser.help() << std::endl;
        return -1;
    }

    auto args = parser.parse(index, argv);
    panda::ecmascript::JSRuntimeOptions runtimeOptions = ArkRuntimeUtils::GetRuntimeOptions();
    if (args["jit"].as<bool>())
    {
        runtimeOptions.SetEnableJIT(true);
    }

    // clang-format off: keep list
    std::unordered_map<std::string, std::pair<RunnerLogLevel, LogLevel>> logLevelMap{
        {"debug", {RunnerLogLevel::DEBUG, LogLevel::Debug}},
        {"info", {RunnerLogLevel::INFO, LogLevel::Info}},
        {"warning", {RunnerLogLevel::WARNING, LogLevel::Warn}},
        {"error", {RunnerLogLevel::ERROR, LogLevel::Error}},
        {"fatal", {RunnerLogLevel::FATAL, LogLevel::Fatal}},
    };
    // clang-format on
    auto userLv = logLevelMap.find(args["log-level"].as<std::string>());
    auto runnerLv = userLv != logLevelMap.end() ? userLv->second : logLevelMap["error"];
    RunnerLogger::SetBaseLogLevel(runnerLv.first);
    SetLogLevel(runnerLv.second);

    auto arkOpts = args["ark-options"].as_optional<std::vector<std::string>>();
    if (arkOpts != std::nullopt)
    {
        auto arkArgs = arkOpts.value();
        const char *runtimeOpt[arkArgs.size() + 1];
        runtimeOpt[0] = argv[0];
        for (size_t i = 0; i < arkArgs.size(); i++)
        {
            if (std::string(arkArgs[i]) == "--help")
            {
                std::cout << "Usage: " << argv[0]
                          << " -k <option> ...\n"
                             "Options:\n"
                          << panda::ecmascript::HELP_OPTION_MSG << std::endl;
                return 0;
            }
            runtimeOpt[i + 1] = arkArgs[i].c_str();
        }
        if (!runtimeOptions.ParseCommand(arkArgs.size() + 1, runtimeOpt))
        {
            std::cerr << "Failed to parse ark options.\n"
                         "use -k --help to check help message."
                      << std::endl;
            return -2;
        }
    }

    ArkRuntimeUtils::Init();

    NapiRunner runner(runtimeOptions);
    if (runner.InitLoop() != 0)
    {
        DEFAULT_LOGGER(ERROR) << "Faild to unref uv_handle.";
        return -3;
    };
    {
        utils::NapiHandleScope scope(runner.GetEnv());
        runner.InitNativeModule();
        napi_env env = runner.GetEnv();
        napi_value global = utils::GetGlobal(env);
        JsProcess::Init(env, global, argc, index, argv);
        runner.LoadTSModule();
    }

    if (runner.RunScript(file) != 0)
    {
        DEFAULT_LOGGER(ERROR) << "Exception occurd in execution.";
        return 1;
    }

    runner.RunLoop(UV_RUN_DEFAULT);

    return JsProcess::GetExitCode();
}
